gRPCを使ってRust – JavaScript通信
Introduction
RPCは昔からあるクライアント−サーバー間の通信手法です。
サーバで実装されている関数をクライアントから呼んで実行します。
最近ではHTTP/HTTPSでクラサバ間通信をして、
フォーマットにXML(XML-RPC)やJSON(JSON-RPC)を利用するのが
多いようです。
(直近で私は使った記憶がないですが)
上記のRPCは各技術がメジャーなので採用しやすいですが、
パフォーマンスがそこまで高くなかったり
バイナリデータが扱いにくかったりします。
そういった問題点を解決するために開発されたのが、
Google発のRPCであるgRPCです。
gRPC?
gRPCはハイパフォーマンスなオープンソースのRPCフレームワークです。
Googleが開発したRPC技術(Stubby)が元となって開発され、オープンソース化されました。
現在はCNCFによって開発が進められています。
gRPCでは、トランスポートにHTTP/2を使い、
データのシリアライズにProtobuf(Protocol Buffers)を使います。
Protocol BuffersはGoogle発のデータフォーマットで、
データを効率よく(バイナリデータも含む)扱うことができます。
APIは、アプリ同士が異なるプログラミング言語・プラットフォームであっても、
やりとりは問題ありません。
gRPCについてはこのへんが詳しいので、
ご確認ください。
Protocol Buffersとprotoファイル
gRPCのフォーマットであるProtocol Buffersは、
やり取りするデータの型を下記のようなファイル(.proto)で定義します。
syntax = "proto3"; package example.foo; message FooRequest { string greet = 1; } message FooResponse { string msg = 1; } service FooService { rpc sayFoo (FooRequest) returns (FooResponse) {} }
↑のように、RPCでやりとりする関数とオブジェクトの定義を明確に定義します。
プロトコル定義ができたら、対象システムで使用する
gRPC用クラスの生成を行います。
Protocol Buffersでは、定義ファイルから各言語に定義された
クラス定義ファイルを生成するツールがあるので、
それを使ってクラスを生成し、実装します。
Environment
- OS : MacOS 12.4
- Rust : 1.64.0
- Node : v18.11.0
Create gRPC Server & Client
では、RustでgRPCサーバを実装し、
Javascriptで実装したgRPCクライアントと通信してみます。
なお、使用している言語やライブラリは違いますがここでも
gRPCを試しているので見てみてください。
Rust(tonic)でgRPCサーバの実装
まずはRustでgRPCサーバの実装をしてみます。
gRPC実装はtonicを使います。
これはRustのgRPCライブラリの中でもメジャーなgRPCライブラリです。
ではプロジェクトの作成をします。
また、protoディレクトリを作成して、
そこにコード生成用のスキーマ定義ファイルを作成します。
% cargo new grpc-server && cd grpc-server % mkdir proto
proto/user.protoファイルを下記のように記述します。
UserService.CreateUserでは、
リクエストで名前と年齢を受け取ると、
Userオブジェクトを返すように定義します。
syntax = "proto3"; package example.user; /* リクエスト用オブジェクト */ message MyRequest { string name = 1; int32 age = 2; } /* レスポンス用オブジェクト */ message Book { string title = 1; string author = 2; } /* レスポンス用オブジェクト */ message User { string name = 1; int32 age = 2; repeated Book books = 3; } /* サービス定義 */ service UserService { rpc CreateUser (MyRequest) returns (User) {} }
Cargo.tomlに依存ライブラリなどの設定を定義します。
tonic-buildはprotoファイルからコード生成するために使います。
build.rsでcargo build時に実行されるように記述します。
[package] name = "grpc-web-server" version = "0.1.0" edition = "2021" build = "build.rs" [dependencies] tonic = "0.8.2" bytes = "1.2.1" prost = "0.11.0" prost-derive = "0.11.0" tokio = { version = "1.0", features = ["full"] } [build-dependencies] tonic-build = "0.8.2"
プロジェクトのルート直下にbuild.rsを作成します。
ここで記述した内容がコンパイル前に実行されます。
なので、cargo build時にprotoファイルからコードが生成され、
それを元にほかのrsファイルがコンパイルされます。
fn main() -> Result<(), Box<dyn std::error::Error>> { tonic_build::compile_protos("proto/user.proto")?; Ok(()) }
src/main.rsにgRPCサーバ用コードを記述します。
include_proto!マクロでprotoファイルをreadし、
各モジュールをimportします。
また、MyUserService構造体を定義して、create_user関数を実装します。
このへんはprotoファイルにあわせて実装しましょう。
use tonic::{transport::Server, Request, Response, Status}; mod user { tonic::include_proto!("example.user"); } use user::{ user_service_server::{UserService, UserServiceServer}, Book, MyRequest,User }; #[derive(Default)] pub struct MyUserService {} #[tonic::async_trait] impl UserService for MyUserService { async fn create_user(&self, request: Request<MyRequest>) -> Result<Response<User>, Status> { let b1 = Book{title:"book1".to_string(),author:"author1".to_string()}; let b2 = Book{title:"book2".to_string(),author:"author2".to_string()}; let books = vec![b1,b2]; let req = request.into_inner(); println!("reqest name : {}",req.name); println!("reqest age : {}",req.age); let reply = user::User { name: format!("{}", req.name).into(), age:req.age, books:books }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let addr = "[::1]:50051".parse().unwrap(); let user = MyUserService::default(); Server::builder() .add_service(UserServiceServer::new(user)) .serve(addr) .await?; Ok(()) }
main関数でUserServiceをバインドしてgRPCサーバを起動します。
cargo runで実行すればサーバの起動完了です。
% cargo run Compiling grpc-web-server v0.1.0 Finished dev [unoptimized + debuginfo] target(s) in 2.17s Running `target/debug/grpc-web-server`
クライアントをJavascriptで実装する前に、
grpcurlというツールをつかってサーバにアクセスしてみましょう。
※grpcurlはgRPCサーバ用curl
Homebrewでインストールします。
% brew install grpcurl
gRPCサーバ宛にprotoファイルを指定してリクエストを送ります。
% grpcurl -proto proto/user.proto -d '{"name":"foo","age":30}' -plaintext localhost:50051 example.user.UserService.CreateUser { "name": "foo", "age": 30, "books": [ { "title": "book1", "author": "author1" }, { "title": "book2", "author": "author2" } ] }
動いてるのでサーバ側はOKです。
JavaScriptでgRPCクライアントの実装
引き続きクライアントの実装に移ります。
適当なディレクトリをつくってgRPC関連のモジュールをインストールします。
% mkdir grpc-client && cd grpc-client % yarn add @grpc/grpc-js grpc-tools % yarn add -D grpc-tools grpc_tools_node_protoc_ts
grpc_tools_node_protocツールをつかってprotoファイルからgRPC用ファイルを生成します。
% cd path/your/grpc-client % yarn run grpc_tools_node_protoc --plugin=./node_modules/.bin/protoc-gen-ts --js_out=import_style=commonjs,binary:codegen --grpc_out=grpc_js:codegen --ts_out=grpc_js:codegen -I /path/your/grpc-web-server/proto /path/your/grpc-web-server/proto/user.proto
必要なコードが生成できたので、client.mjsファイルを
作成してクライアントの実装をします。
import userpb from './codegen/proto/user_pb.js'; import grpc from '@grpc/grpc-js'; import user_grpc from './codegen/proto/user_grpc_pb.js'; //gRPCへ送るリクエストオブジェクトの作成 const request = new userpb.MyRequest(); request.setName("grpc from js"); request.setAge(30); //gRPCサーバ用クライアント作成 const client = new user_grpc.UserServiceClient( "localhost:50051", grpc.credentials.createInsecure() ); //CreateUser関数呼び出しと結果表示 client.createUser(request,function(err,user){ console.log(user.toString()); });
クライアントの実行。
gRPCで通信できました。
% node client.mjs grpc from js,30,book1,author1,book2,author2
Summary
今回はRust-JS間でgRPC通信を試してみました。
同じprotoファイルを起点とすることで
クライアント-サーバ間のインターフェイスを統一し、
Protobufで高速通信もできるのでとても有用です。
(特に最近はマイクロサービス間の通信として使われたりします)
gRPCではストリーミング処理もできたりするので、
興味のあるかたは確認してみてください。